package org.spp.voronoi;

import org.spp.utilities.Point;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.math.util.MathUtils;

/**
 * Edge.java
 * @author Ilio Catallo, Eleonora Ciceri
 * @version 1.0
 * 
 * Description: class that implements the edge of the Voronoi Diagram
 * 
 * Date: Jul 10, 2011
 */
public abstract class Edge {
    
    // Attributes
    protected double minDistance;
    protected Vertex[] vertexes;
    protected double[] distanceCoefficients;
    protected Centroid centroid;
    protected List<Cell> adjCells;
    protected List<Facet> facets;
    protected double[] direction;
    
    /**
     * Costructor for the class Edge
     * @param startVertex
     * @param endVertex 
     */
    public Edge(Vertex startVertex, Vertex endVertex, List<Facet> facets){
        
        this.vertexes = new Vertex[2];
        this.distanceCoefficients = new double[2];
        this.facets = facets;
        
        this.vertexes[0] = startVertex;
        this.vertexes[1] = endVertex;
        
    }
    
    /**
     * Costructor for the class Edge
     * @param startVertex
     * @param endVertex
     * @param centroid 
     */
    public Edge(Vertex startVertex, Vertex endVertex, Centroid centroid, List<Facet> facets){
        
        this(startVertex, endVertex, facets);
        this.centroid = centroid;
        
        this.direction = computeDirection();
        double[] vertexToCentroid = centroid.computeVectorToPoint(startVertex);
        this.minDistance = Point.dotProduct(vertexToCentroid, direction);
        
        this.adjCells = findAdjCells();
    }
    
     /**
     * Method that returns the point of the edge such that its distance wrt
     * the related centroid is minimum
     * @return minDistance 
     */
    public Double getMinDistance() {
        return minDistance;
    }
    
    /**
    * Method that returns the start vertex of the edge
    * @return startVertex
    */
    public Vertex getStartVertex(){ 
        return vertexes[0];
    }
    
    /**
     * Method that retursn the coefficient of the edge distance function
     * @return distanceCoefficients
     */
    public double[] getDistanceCoefficients() {
        return distanceCoefficients;
    }
    
    /**
     * Method that returns the centroid according to which the edge is computed
     * @return centroid
     */
    public Centroid getCentroid() {
        return centroid;
    }
    
    /**
     * Method that returns the cells that share the edge 
     * @return adjCells
     */
    public List<Cell> getAdjCells() {
        return adjCells;
    }
    
    /**
     * Method that returns the facets of the edge
     * @return facets
     */
    public List<Facet> getFacets() {
        return facets;
    }
    
    /**
     * Method that returns the direction of the edge
     * @return direction
     */
    public double[] getDirection() {
        return direction;
    }
    
    /**
     * Method that sets the coefficient for the edge distance function
     * @param distanceCoefficients 
     */
    public void setDistanceCoefficients(double[] distanceCoefficients) {
        this.distanceCoefficients = distanceCoefficients;
    }
    
    /**
     * Method that sets the facets that generate the edge
     * @param facets 
     */
    public void setFacets(List<Facet> facets) {
        this.facets = facets;
    }
    
    /**
     * Method that sets the direction of the edge
     * @param direction 
     */
    public void setDirection(double[] direction) {
        this.direction = direction;
    }
    
    /**
     * Method that computes the direction vector of the edge
     * (To be implemented by Edge2D and Edge3D)
     * @return direction
     */
    protected abstract double[] computeDirection();
    
    /**
     * Given the list of factes that create the edge, the method finds which cells
     * have in common the edge (to be implemented by Edge2D and Edge3D)
     * @return adjCells 
     */
    protected abstract List<Cell> findAdjCells();
    

    /**
     * Method that checks if the direction computed for a given edge is correct. If it is not
     * the case the method takes care of invert the signs of the direction vector
     * @param direction
     * @return correctedDirection
     */
    protected double[] correctDirection(double[] newDirection) {
        // Variables
        Vertex vertex = vertexes[0];
        int numCentroids = vertex.getCells().size();        
        double[] shiftedVertex = new double[newDirection.length];
        
        for (int i = 0; i < shiftedVertex.length; i++)
            shiftedVertex[i] = vertex.getCoordinates()[i] + 100*MathUtils.EPSILON*newDirection[i];
                    
        
        // Case in which there are only two centroids
        if (numCentroids < 3){
            double[] centroidCoordinates = centroid.getCoordinates();
            
            // Compute vector to shifted vertex and distance
            double[] vectorToShiftedVertex = new double[shiftedVertex.length];
            
            for (int i = 0;i < vectorToShiftedVertex.length; i++)
                vectorToShiftedVertex[i] = centroidCoordinates[i] - shiftedVertex[i];
           
            double distanceToShiftedVertex = MathUtils.safeNorm(vectorToShiftedVertex);
            
            // Compute vector to vertex and distance
            double[] vectorToVertex = new double[vertex.getCoordinates().length];
            for (int i = 0;i < vectorToVertex.length; i++)
                vectorToVertex[i] = centroidCoordinates[i] - vertex.getCoordinates()[i];
   
            double distanceToVertex = MathUtils.safeNorm(vectorToVertex);  
            
            // If we get closer to the current centroid the direction is correct, otherwise we have to invert it
            if (distanceToShiftedVertex > distanceToVertex){
                
                for (int i = 0; i < newDirection.length; i++)
                    newDirection[i] = newDirection[i] * (-1);
  
            }
        }
        // Case in which there are three or more centroids
        else {
            // Variables
            List<Cell> adjCells= vertex.getCells();
            
            
            Double[] distances = new Double[numCentroids];
            double changedDistance = 0;
            double unchangedDistance = 0;
            List<Double> firstDistanceValue = new ArrayList<Double>();
            List<Double> secondDistanceValue = new ArrayList<Double>();
            
            for (int i = 0; i < numCentroids; i++) {
                
                Centroid  adjCentroid = adjCells.get(i).getCentroid();
                distances[i] = adjCentroid.computeDistance(new Vertex3D(shiftedVertex));
                        
                if (i > 0)
                    if (Math.abs(distances[i] - distances[0]) > MathUtils.EPSILON)
                         firstDistanceValue.add(distances[i]);
                    else
                         secondDistanceValue.add(distances[i]);
                else
                    secondDistanceValue.add(distances[i]);
            }
           
            // degenerate case: an edge common to all cells, with all the centroids on the same half-space
            if (firstDistanceValue.isEmpty()){

                double priorDistance = adjCells.get(0).getCentroid().computeDistance(vertex); 

                // all the distances from the shifted vertex are greater than the distance from the vertex, so we moved away from the vertex
                if (priorDistance < secondDistanceValue.get(0)){
                    changedDistance = 1;
                    unchangedDistance = 2;

                } 
                // all the distances from the shifted vertex are less than the distance from the vertex, so moved on the edge
                else {
                    changedDistance = 2;
                    unchangedDistance = 1;
                }
            }
            // at least one of the centroid is in a different half-space wrt all the remaining centroids        
            else{
                
                if (firstDistanceValue.size() == 1) {
                    changedDistance = firstDistanceValue.get(0);
                    unchangedDistance = secondDistanceValue.get(0);
                } 
                else{
                    changedDistance = secondDistanceValue.get(0);
                    unchangedDistance = firstDistanceValue.get(0);
                }
               
            }
             
            // Decide wether to invert or not the direction 
            if (changedDistance < unchangedDistance){ 
                
                for (int i = 0; i < newDirection.length; i++)
                    newDirection[i] = newDirection[i] * (-1);
            }                   
                
        }
        
        return newDirection;
    }
    
    /**
     * Override the Object.equals function to check if two edges have the same
     * value of minDistance and if they share the same cells 
     * (adjCells and centroid.cell)
     * @param e
     * @return isEqual
     */
    @Override
    public boolean equals(Object e){
        // Variables
        List<Cell> adjCells1 = new ArrayList<Cell>();
        List<Cell> adjCells2 = new ArrayList<Cell>();
        
        //Check if e is actually a Edge
        if (!(e instanceof Edge))
            return false;
            
        // Add the adjacent cells if any
        if (this.adjCells != null)
            adjCells1.addAll(this.adjCells);
        
        if (((Edge) e).getAdjCells() != null)
            adjCells2.addAll(((Edge) e).getAdjCells());
        
        // Add the current cell
        adjCells1.add(this.centroid.getCell());
        adjCells2.add(((Edge) e).getCentroid().getCell());
        
        // Check if the two edges have the same minDistance and same cells
        if (Math.abs(this.minDistance - ((Edge) e).getMinDistance()) > MathUtils.EPSILON)
            return false;
        
        if (!adjCells1.containsAll(adjCells2))
            return false;
        
        return true;
    }

    /**
     * Object hashCode override
     * @return hash
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 43 * hash + (int) (Double.doubleToLongBits(this.minDistance) ^ (Double.doubleToLongBits(this.minDistance) >>> 32));
        hash = 43 * hash + Arrays.deepHashCode(this.vertexes);
        hash = 43 * hash + Arrays.hashCode(this.distanceCoefficients);
        hash = 43 * hash + (this.centroid != null ? this.centroid.hashCode() : 0);
        hash = 43 * hash + (this.adjCells != null ? this.adjCells.hashCode() : 0);
        return hash;
    }

}